package org.jugile.util;

/*

Copyright (C) 2007-2011 Jukka Rahkonen  email: jukka.rahkonen@iki.fi

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

*/

import java.io.Serializable;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

/**
 * "No man can serve two masters: for either he will hate the one, and love the other; 
 *  or else he will hold to the one, and despise the other. 
 *  Ye cannot serve God and mammon." (Math. 6:24)
 * 
 * ==========
 * 
 * Immutable Money class for all money type handling consistently.
 * 
 * Money should not be any kind of decimal number. No BigDecimal or any other
 * floating point number. Money should be long integer, representing cents.
 * Sometimes prices can have even fragments of cents and they can be presented
 * as BigDecimal. Money is an accountable amount of money that can be part of
 * money transaction. (RAHKOJUK)
 * 
 * Btw, BigDecimal is buggy. (2009). Try how long does it take to run this simple loop:
 * BigDecimal bd = new BigDecimal("1.0"); for (int i = 0; i < 1000; i++) { bd =
 * bd.multiply(bd); } System.out.println("result: " + bd); (RAHKOJUK)
 * 
 * @author jukka.rahkonen@iki.fi
 */
public final class Money extends Jugile implements Serializable, Comparable<Money> {
	private static final long serialVersionUID = 1L;
	private static final Locale LOCALE_FI = new Locale("fi", "FI");

	public enum Currency {
		EUR, FIM,
	}

	private final long cents;			// immutable
	private final Currency currency; 	// immutable

	public Money() { 
		this.cents = 0;
		this.currency = Currency.EUR;
	}
	public Money(long cents) {
		this.cents = cents;
		this.currency = Currency.EUR;
	}

	public Money(long cents, Currency c) {
		this.cents = cents;
		this.currency = c;
	}

	public Money(double d) {
		this.cents = toCents(d);
		this.currency = Currency.EUR;
	}

	public Money(double d, Currency c) {
		this.cents = toCents(d);
		this.currency = c;
	}

	public Money(String str) {
		cents = parse(str);
		currency = Currency.EUR;
	}

	public Money(String str, Currency c) {
		cents = parse(str);
		currency = c;
	}

	public Money(BigDecimal bd) {
		if (bd == null)
			cents = 0;
		else
			cents = toCents(bd.doubleValue());
		currency = Currency.EUR;
	}

	public Money(BigDecimal bd, Currency c) {
		if (bd == null)
			cents = 0;
		else
			cents = toCents(bd.doubleValue());
		currency = c;
	}

	public long getCents() {
		return cents;
	}
	
	public Currency getCurrency() {
		return currency;
	}

	public BigDecimal getBigDecimal() {
		BigDecimal c = new BigDecimal(cents);
		return c.divide(new BigDecimal(100));
	}

	/**
	 * Get the integer value without cents. eg. 1,55 euros return 1.
	 */
	public int getInteger() {
		return (int)cents/100;
	}

	/**
	 * Get rounded value to nearest integer without cents. eg. 1,49 euros return
	 * 1.
	 */
	public int getRoundedInteger() {
		return (int) Math.round(nextUp(cents / 100.0));
	}

	public String toString() {
		if (cents == 0)
			return "0,00";
        DecimalFormatSymbols symbols = new DecimalFormatSymbols(LOCALE_FI);
        symbols.setGroupingSeparator(' ');
        DecimalFormat f = new DecimalFormat("#,##0.00", symbols);
        f.setDecimalSeparatorAlwaysShown(true);
        f.setMaximumFractionDigits(2);
        f.setMinimumFractionDigits(2);
        f.setMinimumIntegerDigits(1);
        return f.format(cents / 100.0);
    }

	public String toStringIntegerValueWithCurrency() {
		String c = "€";
		if (currency == Currency.FIM)
			c = "mk";
		return getInteger()+ " " + c;
	}
	
	public String toStringWithCurrency() {
		String c = "€";
		if (currency == Currency.FIM)
			c = "mk";
		return toString() + " " + c;
	}

	private long parse(String str) {
		if (str == null || str.trim().length() == 0) return 0;
		// remove all whites
		str = str.trim();
		str = str.replaceAll("\\s", "");
		str = str.replaceAll(" ", ""); // some other white space (utf8?)
		str = str.replaceAll(",", ".");
		double d = Double.parseDouble(str);
		return toCents(d);
	}

	/**
	 * Does the correct rounding for positive double values to cents.
	 */
	public static long toCents(double d) {
		// nextUp is needed for getting 1.005 correctly rounded to 1.01
		return Math.round(nextUp(d * 100.0));
	}

	public Money plus(Money m) {
		assertSameCurrency(m);
		return new Money(cents + m.cents, currency);
	}

	public Money minus(Money m) {
		assertSameCurrency(m);
		return new Money(cents - m.cents, currency);
	}

	public Money neg() {
		return new Money(-this.cents, currency);
	}

	public int compareTo(Money m) {
		assertSameCurrency(m);
		return (new Long(cents)).compareTo(m.cents);
	}

	public Money mult(double d) {
		return new Money(toCents((cents / 100.0) * d), currency);
	}

	public Money divBy(double d) {
		if (d == 0)
			throw new RuntimeException("divided by zero");
		return new Money(toCents((cents / 100.0) / d), currency);
	}

	/**
	 * Common case for dividing Money to some parts where rounded amounts of
	 * parts must equals to original sum.
	 * 
	 * @param numberOfParts
	 *            number of parts to divide to.
	 * @return List of Money parts. Last one has rounding correction (modulo).
	 */
	public List<Money> divToParts(int numberOfParts) {
		if (numberOfParts == 0)
			throw new RuntimeException("divided by zero");
		List<Money> res = new ArrayList<Money>();
		Money a = divBy(numberOfParts);
		long modulo = cents - numberOfParts * a.cents;
		for (int i = 0; i < numberOfParts; i++) {
			Money m = new Money(a.cents);
			if (i == (numberOfParts - 1))
				m = new Money(a.cents + modulo);
			res.add(m);
		}
		return res;
	}

	/**
	 * @param d
	 * @return next double towards positive infinity
	 */
	private static double nextUp(double d) {
		if (Double.isNaN(d) || d == (1.0D / 0.0D)) {
			return d;
		} else {
			d += 0.0D;
			return Double.longBitsToDouble(Double.doubleToRawLongBits(d)
					+ (d < 0.0D ? -1L : 1L));
		}
	}

	private void assertSameCurrency(Money m) {
		if (m.currency != currency)
			throw new RuntimeException("currency mismatch");
	}


	public boolean equals(Money m) {
		if (m == null) return false;
		return m.cents == cents;
	}
    
}
